package com.cordys.coe.tools.useradmin.util;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.SimpleDateFormat;
import java.util.Calendar;

/**
 * A passwordhasher class to generates MD5 / SHA-1 password / PasswordDigest hashes.
 * For PBKDF2WithHmacSHA1, use the dedicated webservice operations in Cordys.
 * 
 * @author jpluimers 
 */
public class PasswordHashAndDigest 
{
	// password hashing algorithm
	public static final int PW_HA_NO_HASH = 0;
	public static final int PW_HA_MD5 = 1;
	public static final int PW_HA_SHA1 = 2;
	public static final int PW_HA_MD5_SHA = 3;
	public static final int PW_HA_PBKDF2WithHmacSHA1 = 4;
	
	public static final String SHA1 = "SHA1";
	public static final String SHA1_prefix = "{"+SHA1+"}";
	public static final String MD5 = "MD5";
	public static final String MD5_prefix = "{"+MD5+"}";
	public static final String MD5_SHA = "MD5 + SHA";
	public static final String MD5_SHA_prefix = "{"+MD5_SHA+"}";
	public static final String PBKDF2WithHmacSHA1 = "PBKDF2WithHmacSHA1";
	public static final String PBKDF2WithHmacSHA1_prefix = "{"+PBKDF2WithHmacSHA1+"}";	
	
	private static final String SHA1_PASSWORD_ENCODING = null; // Can be set to a specific encoding
	
	/**
	 * Generate the Password hash for the given password.
	 * 
	 * @param password
	 * @param algorithm
	 * @param prefixKey
	 * @param useDefaultEncoding
	 * @return
	 */
	private static String generatePasswordHash(String password, String algorithm, String prefixKey, boolean useDefaultEncoding) 
	{
		try	
		{
			MessageDigest digest = MessageDigest.getInstance(algorithm);			
			if(useDefaultEncoding) 
			{
				digest.update(password.getBytes());
			} else 
			{
				for(char c : password.toCharArray()) 
				{
					digest.update((byte) (c>>8));
					digest.update((byte) c);
				}
			}
			byte[] digestedPassword = digest.digest();
			char[] encodedDigestedChars = Base64Coder.encode(digestedPassword);
			return prefixKey + new String(encodedDigestedChars);
		}
		catch (NoSuchAlgorithmException ne) 
		{
			return password;
		}
	}

	/** Calculate digest according to WS-Security UserNameTokenProfile.
	 * 
	 * @param password password
	 * @param nonce
	 * @param creationDate UTC creation date
	 * @return Base64 encoded digest
	 * @throws UnsupportedEncodingException 
	 * @throws NoSuchAlgorithmException 
	 */
	public static String generatePasswordDigest(String password, String nonce, String creationDate)	throws UnsupportedEncodingException, NoSuchAlgorithmException 
	{
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		
		String b64Password = generatePasswordHash(password, SHA1, "", false);
		
		addBytes(bos, nonce, SHA1_PASSWORD_ENCODING);
		addBytes(bos, creationDate, SHA1_PASSWORD_ENCODING);
		addBytes(bos, b64Password, SHA1_PASSWORD_ENCODING);
		
		return generatePasswordHash(bos.toString(), SHA1, "", true);
	}
	
	private static void addBytes(ByteArrayOutputStream os, String value, String encoding) throws UnsupportedEncodingException 
	{
		if (encoding == null) 
		{
			for (char c : value.toCharArray()) 
			{
				os.write((byte) (c >> 8));
				os.write((byte) c);
			}		
		} else 
		{
			byte[] bytes = value.getBytes(encoding);
			for(byte b : bytes) 
			{
				os.write(b);
			}
		}
	}

    public static String now() 
    {
    	Calendar cal = Calendar.getInstance();
    	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
    	return sdf.format(cal.getTime());
    }
  
    private static SecureRandom random = new SecureRandom(); // Expensive !! 

    public static String nextNonce() 
    {
    	return new BigInteger(130, random).toString(32);
    }
	
    /**
     * Generate a sha1 password hash
     * 
     * @param password
     * @return
     * @throws UnsupportedEncodingException
     * @throws NoSuchAlgorithmException
     */
  	public static String getSha1PasswordHash(String password) 
  	{
		String hashPassword = generatePasswordHash(password, SHA1, SHA1_prefix, false);
		return hashPassword;
  	}
	
    /**
     * Generate a MD5 password hash
     * 
     * @param password
     * @return
     */
  	public static String getMD5PasswordHash(String password) 
  	{
		String hashPassword = generatePasswordHash(password, MD5, MD5_prefix, false);
		return hashPassword;
  	}
  	
    /**
     * Generate a MD5 + SHA1 password hash
     * This method can be used to validate a password as generated by the password convert utility.
     * 
     * @param password
     * @return
     */
  	public static String getMD5_SHA1PasswordHash(String password) 
  	{
		String md5HashPassword = generatePasswordHash(password, MD5, "", false);
		String md5_sha1hashPassword = generatePasswordHash(md5HashPassword, SHA1, MD5_SHA_prefix, false);
		return md5_sha1hashPassword;
  	}
  	
  	/**
  	 * Generate a a PasswordDigest hash
  	 * 
  	 * @param password
  	 * @return
  	 */
    public static String getPasswordDigestHash(String password) throws UnsupportedEncodingException, NoSuchAlgorithmException 
    {
		String passwordDigest = null;				
		String nonce = nextNonce();
		String creationDate	= now();
		
		passwordDigest = generatePasswordDigest(password, nonce, creationDate);
		return passwordDigest;
	}  
    
    /**
     * Generate pw hash as per the given algorithm
     * 
     * @param password
     * @param hashingAlgorithm		PasswordHashAndDigest.PW_HA_MD5, PW_HA_SHA1 or PW_HA_MD5_SHA
     * @return
     */
    public static String getPasswordHash(String password, int hashingAlgorithm)
    {
    	String result = password;
    	
    	switch (hashingAlgorithm)
    	{
	    	case PW_HA_MD5:
	    	{
	    		result = getMD5PasswordHash(password);
	    		break;
	    	}
	    	case PW_HA_SHA1:
	    	{
	    		result = getSha1PasswordHash(password);
	    		break;
	    	}
	    	case PW_HA_MD5_SHA:
	    	{
	    		result = getMD5_SHA1PasswordHash(password);
	    		break;
	    	}	    	
    	}
    	return result;
    }
    
    /**
     * Returns whether the given string contains a password hash as supported in LDAP for auth users.
     * 
     * @param pw
     * @return
     */
    public static boolean pwIsHashed(String pw)
    {
    	boolean result = false;
    	if (Util.isSet(pw))
    	{
    		result = (pw.startsWith(SHA1_prefix) || pw.startsWith(MD5_prefix) || pw.startsWith(MD5_SHA_prefix) || pw.startsWith(PBKDF2WithHmacSHA1_prefix));
    	}
    	return result;
    }
    
    /**
     * Returns the hashing algorithm as used for the given password hash.
     * 
     * @param pwHash 
     * @return				PasswordHashAndDigest.PW_HA_NO_HASH, PW_HA_MD5, PW_HA_SHA1 or PW_HA_MD5_SHA
     */
    public static int getPasswordHashingAlgorithm(String pwHash)
    {
    	int result = PW_HA_NO_HASH;
    	if (Util.isSet(pwHash))
    	{
    		if (pwHash.startsWith(SHA1_prefix))
    		{
    			result = PW_HA_SHA1;
    		}
    		else if (pwHash.startsWith(MD5_prefix))
    		{
    			result = PW_HA_MD5;
    		}
    		else if (pwHash.startsWith(MD5_SHA_prefix))
    		{
    			result = PW_HA_MD5_SHA;
    		}   
    		else if (pwHash.startsWith(PBKDF2WithHmacSHA1_prefix))
    		{
    			result = PW_HA_PBKDF2WithHmacSHA1;
    		}      		
    	}
    	return result;
    }
    
    /**
     * Get the code as corresponding to the given algorithm.
     * 
     * @param hashingAlgorithm
     * @return
     */
    public static String getAlgorithmCode(int hashingAlgorithm)
    {
    	String result = "UNKNOWN";
    	switch (hashingAlgorithm)
    	{
	    	case PW_HA_MD5:
	    	{
	    		result = MD5;
	    		break;
	    	}
	    	case PW_HA_SHA1:
	    	{
	    		result = SHA1;
	    		break;
	    	}
	    	case PW_HA_MD5_SHA:
	    	{
	    		result = MD5_SHA;
	    		break;
	    	}
	    	case PW_HA_PBKDF2WithHmacSHA1:
	    	{
	    		result = PBKDF2WithHmacSHA1;
	    		break;
	    	}	    	
    	}   
    	return result;
    }
    
    /**
     * Generate a new password
     * 
     * @return
     */
	public static String generatePassword() 
	{
		String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
		String pin = "";
		for (int i = 0; i < 6; i++) 
		{
			pin = pin + str.charAt((int) ((Math.random() * 100000) % 62));
		}
		return pin;
	}    
}
